⚡ 30 秒速记
- 多子节点比较要同时处理身份与顺序
- 没键通常按位置更新,有键按业务身份匹配
- 先确保内容正确,再安排移动、增加和删除
- 旧索引序列帮助找可保持位置的节点
- 重复键会让匹配失去可靠依据
列表差异比较是在核对谁还在、谁换了位置、谁新增或离开。 稳定键让渲染器找到同一个业务项,复用节点后仍要更新它的属性和孩子。没有键时按位置复用虽然能显示结果,却可能把局部状态留给另一条数据。确定匹配关系后,再根据前后顺序安排移动,避免全部删除重建;具体最少移动策略取决于使用的算法。
版本校准: 本文出现 Object.defineProperty、Dep、Watcher 和双端 Diff 时,主要描述 Vue 2 实现;Vue 3 使用 Proxy、effect 与新的渲染器路径。Vue 2 已于 2023-12-31 结束维护,新项目应以 Vue 3 为基线,旧项目参考 Vue 2 EOL 官方说明 制定迁移与安全策略。
# 减小DOM操作的性能开销
上一章我们讨论了渲染器是如何更新各种类型的 VNode 的,实际上,上一章所讲解的内容归属于完整的 Diff 算法之内,但并不包含核心的 Diff 算法。那什么才是核心的 Diff 算法呢?看下图:

我们曾在上一章中讲解子节点更新的时候见到过这张图,当时我们提到只有当新旧子节点的类型都是多个子节点时,核心 Diff 算法才派得上用场,并且当时我们采用了一种仅能实现目标但并不完美的算法:遍历旧的子节点,将其全部移除;再遍历新的子节点,将其全部添加,如下高亮代码所示:
function patchChildren(
prevChildFlags,
nextChildFlags,
prevChildren,
nextChildren,
container
) {
switch (prevChildFlags) {
// 省略...
// 旧的 children 中有多个子节点
default:
switch (nextChildFlags) {
case ChildrenFlags.SINGLE_VNODE:
// 省略...
case ChildrenFlags.NO_CHILDREN:
// 省略...
default:
// 新的 children 中有多个子节点
// 遍历旧的子节点,将其全部移除
for (let i = 0; i < prevChildren.length; i++) {
container.removeChild(prevChildren[i].el)
}
// 遍历新的子节点,将其全部添加
for (let i = 0; i < nextChildren.length; i++) {
mount(nextChildren[i], container)
}
break
}
break
}
}
为了便于表述,我们把这个算法称为:简单 Diff 算法。简单 Diff 算法虽然能够达到目的,但并非最佳处理方式。我们经常会遇到可排序的列表,假设我们有一个由 li 标签组成的列表: